/*
 * SRMI (an easy-to-use zero-code remoting library for Java)
 * Copyright (C) 2007  Mohammad Nabil Hussein
 * mohammad (dot) nabil (dot) h (at) gmail (dot) com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package SRMI.RemotingServices;

import SRMI.RemotingServices.ClassGeneration.ClassGenerator;
import SRMI.RemotingServices.ClassGeneration.Server.RmiServiceWrapperGenerator;
import SRMI.SRMIException;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.apache.log4j.Logger;

/**
 * This class is for managing server side. Declaring/Remoting objects is done using
 * this class.
 * @author meemo
 */
public class ServerUtils
{
    // k=type:uuid, v=remote object (generated)
    private static Map<String,Object> our_bindings = new HashMap<String,Object>();
    
    // i put the UUIDs in the key cuz i don't want to depend on the getHashCode() of the object,
    // i.e. the lookup i want must be object-state-independant!
    private static Map<UUID,Object> object_uuids = new HashMap<UUID,Object>();
    
    private static final Logger l = Logger.getLogger(ServerUtils.class);
    
    static
    {
        InstallShutDownHook();
    }
    
    private static boolean shutdownHookAdded = false;
    private synchronized static void InstallShutDownHook()
    {
        if(shutdownHookAdded) return;
        l.info("Adding shutdown hook");
        java.lang.Runtime.getRuntime().addShutdownHook(new Thread(new Runnable()
        {
            public void run()
            {
                l.info("In the shutdown hook");
                
                if(our_bindings.isEmpty())
                {
                    l.info("No bindings to unbind");
                    return;
                }
                
                try
                {
                    LocateRegistry.getRegistry( CommonUtils.registryPortNumber ).list();
                }
                catch(Exception ex)
                {
                    // if we got here, then there is no registry and we should
                    // not unbind anything
                    l.warn("Can't find registry, not unbinding anything");
                    return;
                }
                
                try
                {
                    for(Map.Entry<String,Object> entry : our_bindings.entrySet())
                    {
                        String service = entry.getKey();
                        Naming.unbind(service);
                        l.info("Unbinding " + service);
                    }
                }
                catch (Exception ex)
                {
                    l.error("Exception while unbinding in the shutdown hook",ex);
                }
            }
        }));
        shutdownHookAdded = true;
    }
    
    public ServerUtils()
    {}
    
    /**
     * Allow the wrapped object to be available for other machines. Other 
     * machine will have to find it using @link{ClientUtils.getRemoteObject}.
     * @param wrappedObject The object wanted to be called from other machines.
     * @return The result of calling ClientUtils.getRemoteObject to get 
     * that object. If returned successfully, then the object have been 
     * remoted successfully.
     * @throws SRMI.SRMIException
     */
    public static Object DeclareObject(Object wrappedObject) throws SRMIException
    {
        return DeclareObject(wrappedObject,null);
    }
    
    /** 
     * Allow the wrapped object to be available for other machines using that specific UUID.
     * Other machine will have to find it using @link{ClientUtils.getRemoteObject}.
     * @param wrappedObject The object wanted to be called from other machines.
     * @param uuid The UUID the uniquely defines that object. I uuid == null, one will be
     * created.
     * @return The result of calling ClientUtils.getRemoteObject to get 
     * that object. If returned successfully, then the object have been 
     * remoted successfully.   
     * @throws SRMI.SRMIException
     */
    public static Object DeclareObject(Object wrappedObject,UUID uuid) throws SRMIException 
    {
        InstallShutDownHook();
        assert shutdownHookAdded;
        
        if(java.lang.System.getProperty("srmi.basedir") == null)
        {
            l.warn("System property srmi.basedir is not set! quitting");
            return null;
        }
        
        String serviceFullyQualifiedClassName = wrappedObject.getClass().getCanonicalName();
        String serverIP = CommonUtils.detectExternalInterfaceIP();
        
        // do not declare an object twice !
        if(LookupUUID(wrappedObject) != null)
        {
            l.info("Object " + wrappedObject + ":" + LookupUUID(wrappedObject) + " already declared, reusing");
            return (Remote) our_bindings.get(
                    String.format("//%s:%d/%s:%s",
                    serverIP,CommonUtils.registryPortNumber,serviceFullyQualifiedClassName,LookupUUID(wrappedObject).toString()));
        }
        
        l.info("Declaring " + wrappedObject);
        
        //must be set before the UnicastRemoteObject ( server ) is constructed
        java.lang.System.setProperty("java.rmi.server.hostname",serverIP);
        Registry rmiRegistry = CommonUtils.getRmiRegistry(CommonUtils.registryPortNumber);
                
        ClassGenerator serviceWrapper = new RmiServiceWrapperGenerator(wrappedObject.getClass());
        
        Remote wrapper = null;
        try
        {
            wrapper = (Remote) serviceWrapper.GetGenerated().
                getConstructor(wrappedObject.getClass()).
                newInstance(wrappedObject);
        }
        catch(Exception ex)
        {
            throw new SRMIException("Cant' instantiate the generated wrapper!",ex);
        }
        
        UUID wrappedUUID = uuid==null?
            CommonUtils.GetNewUUID(wrappedObject):
            uuid;
        
        String existingServices[] = null;
        try
        {
            existingServices = rmiRegistry.list();
        }
        catch(Exception ex)
        {
            throw new SRMIException("Can't get the list of services in the RMI Registry", ex);
        }
        
        printExistingServices(existingServices);
        
        String thisServiceInstanceURL = String.format("//%s:%d/%s:%s",
                serverIP,CommonUtils.registryPortNumber,serviceFullyQualifiedClassName,wrappedUUID.toString());
        
        try
        {
            Naming.bind(thisServiceInstanceURL, wrapper);
        }
        catch(Exception ex)
        {
            throw new SRMIException("Can't bind the wrapper",ex);
        }
        
        our_bindings.put(thisServiceInstanceURL,wrapper);
        object_uuids.put(wrappedUUID,wrappedObject);
        l.info("Bound " + wrappedObject + " to " + thisServiceInstanceURL);
        return ClientUtils.getRemoteObject(wrappedObject.getClass(),"localhost",wrappedUUID);
    }
    
    private static void printExistingServices(final String existingServices[])
    {
        l.debug(String.format("Found %d entries in the registry:", existingServices.length));
        for (String existingService : existingServices)
        {
            l.debug("\t+ " + existingService);
        }
    }
    
    /**
     * This functions looks up the 'wrapped object' O in the internal data structure.
     * @param O the 'wrapped object' to look it UUID up.
     * @return The UUID of the 'wrapped object'
     */
    public static UUID LookupUUID(Object O)
    {
        // this function loops on the object_uuids and compare one by one,
        // the 'references' itselves
        for(Map.Entry<UUID,Object> entry : object_uuids.entrySet())
        {
            if(entry.getValue() == O) // Yup, that is 'reference' comparison !
            {
                return entry.getKey();
            }
        }
        l.info("Object reference for the specific instance of " + O.getClass().getName() + "@" + O.hashCode() +
                " not found in declared objects");
        return null;
    }
}